package com.clei.utils;

import com.clei.dto.db.ColumnProperty;
import com.clei.enums.DatabaseServerEnum;
import com.clei.mapper.CommonMapper;
import com.clei.utils.helper.DBHelper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.Reader;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
 * 快速获取某表的 ResultMap信息，以及所有的字段名信息
 *
 * @author KIyA
 * @since 2019-10-17
 */
public final class MybatisUtil {
    /**
     * dbServer
     */
    private static DatabaseServerEnum dbServer = DatabaseServerEnum.MYSQL;

    /**
     * env
     */
    private static String env = dbServer.name().toLowerCase();

    /**
     * dbHelper
     */
    private static DBHelper dbHelper = dbServer.getDbHelper();

    /**
     * session
     */
    private static SqlSession session;

    /**
     * output path
     */
    private static final String OUTPUT_PATH = "E:\\workTemp\\mybatis\\";

    /**
     * 换行 Line Feed
     */
    private static final char LF = '\n';

    private MybatisUtil() {
    }

    /**
     * setDb
     *
     * @param db DatabaseServerEnum
     */
    public static void setDb(DatabaseServerEnum db) {
        dbServer = db;
        env = dbServer.name().toLowerCase();
        dbHelper = dbServer.getDbHelper();
    }

    /**
     * 执行多次操作
     *
     * @param task task
     */
    public static void execMultipleOperation(Runnable task) {
        execMultipleOperation(null, null, task);
    }

    /**
     * 执行多次操作
     *
     * @param dbServer DatabaseServerEnum
     * @param task     task
     */
    public static void execMultipleOperation(DatabaseServerEnum dbServer, Runnable task) {
        execMultipleOperation(dbServer, null, task);
    }

    /**
     * 执行多次操作
     *
     * @param batch 是否是批处理
     * @param task  task
     */
    public static void execMultipleOperation(Boolean batch, Runnable task) {
        execMultipleOperation(null, batch, task);
    }

    /**
     * 执行多次操作
     *
     * @param dbServer DatabaseServerEnum
     * @param batch    是否是批处理
     * @param task     task
     */
    public static void execMultipleOperation(DatabaseServerEnum dbServer, Boolean batch, Runnable task) {
        startMultipleOperation(dbServer, batch);
        task.run();
        stopMultipleOperation(batch);
    }

    /**
     * 普通单次数据库操作
     *
     * @param operation 数据库操作
     * @param <R>       返回类型
     * @return 返回类型
     */
    public static <R> R commonSingleOperation(Function<CommonMapper, R> operation) {
        return singleOperation(CommonMapper.class, operation);
    }

    /**
     * 单次数据库操作
     *
     * @param operation 数据库操作
     * @param <T>       mapper
     * @param <R>       返回类型
     * @return 返回类型
     */
    public static <T, R> R singleOperation(Class<T> clazz, Function<T, R> operation) {
        try (SqlSession session = getSession()) {
            T mapper = session.getMapper(clazz);
            return operation.apply(mapper);
        } catch (Exception e) {
            PrintUtil.log("数据库操作出错", e);
        }
        return null;
    }

    /**
     * 普通多次数据库操作
     *
     * @param operation 数据库操作
     * @param <R>       返回类型
     * @return 返回类型
     */
    public static <R> R commonMultipleOperation(Function<CommonMapper, R> operation) {
        return multipleOperation(CommonMapper.class, operation);
    }

    /**
     * 多次数据库操作
     *
     * @param operation 数据库操作
     * @param <T>       mapper
     * @param <R>       返回类型
     * @return 返回类型
     */
    public static <T, R> R multipleOperation(Class<T> clazz, Function<T, R> operation) {
        try {
            T mapper = session.getMapper(clazz);
            return operation.apply(mapper);
        } catch (Exception e) {
            PrintUtil.log("数据库操作出错", e);
        }
        return null;
    }

    /**
     * 输出mapper文件和java类文件
     *
     * @param database      数据库
     * @param table         表
     * @param entityPackage 实体类包
     * @param mapperPackage mapper包
     */
    public static void writeMapperAndClass(String database, String table, String entityPackage, String mapperPackage) {
        List<Map<String, String>> columnList = commonSingleOperation(mapper -> mapper.selectMysqlColumn(database, table));
        String objName = StringUtil.snake2Camel(table);
        String simpleClassName = StringUtil.snake2Camel(table, true);
        String simpleMapperName = simpleClassName + "Mapper";
        String className = entityPackage + '.' + simpleClassName;
        String mapperClassName = mapperPackage + '.' + simpleMapperName;
        // mapper
        StringBuilder mapper = new StringBuilder();
        mapper.append("package ")
                .append(entityPackage)
                .append(';')
                .append(LF)
                .append(LF)
                .append("@Mapper")
                .append(LF)
                .append("public interface ")
                .append(simpleMapperName)
                .append(" {")
                .append(LF);
        // entity
        StringBuilder entity = new StringBuilder();
        entity.append("package ")
                .append(entityPackage)
                .append(';')
                .append(LF)
                .append(LF)
                .append("public class ")
                .append(simpleClassName)
                .append(" {")
                .append(LF);

        PrintUtil.log("list : " + columnList);
        // mapperXml
        StringBuilder mapperXml = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
                .append(LF)
                .append("<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">")
                .append(LF)
                .append(LF)
                .append("<mapper namespace=\"")
                .append(mapperClassName)
                .append("\">")
                .append(LF);
        // baseMap
        StringBuilder baseMap = new StringBuilder("<resultMap id=\"BaseMap\" type=\"")
                .append(className)
                .append("\">")
                .append(LF);
        StringBuilder baseColumn = new StringBuilder("<sql id=\"BaseColumn\">")
                .append(LF);
        // selectById
        StringBuilder selectByIdMethod = new StringBuilder("<select id=\"selectById\" parameterType=\"");
        // deleteById
        StringBuilder deleteByIdMethod = new StringBuilder("<delete id=\"deleteById\" parameterType=\"java.util.List\">");
        // insert
        StringBuilder insertMethod = new StringBuilder("<insert id=\"insert\" parameterType=\"")
                .append(className)
                .append("\" useGeneratedKeys=\"true\" keyProperty=\"id\">")
                .append(LF)
                .append("INSERT INTO ")
                .append(table);
        StringBuilder insertColumn = new StringBuilder();
        StringBuilder insertProperty = new StringBuilder();
        StringBuilder replaceColumn = new StringBuilder();
        // insertBatch
        StringBuilder insertBatchMethod = new StringBuilder("<insert id=\"insertBatch\" parameterType=\"java.util.List\" useGeneratedKeys=\"true\" keyProperty=\"id\">")
                .append(LF)
                .append("INSERT INTO ")
                .append(table);
        // insertOrUpdateBatch
        StringBuilder insertOrUpdateBatchMethod = new StringBuilder("<insert id=\"insertOrUpdateBatch\" parameterType=\"java.util.List\">")
                .append(LF)
                .append("INSERT INTO ")
                .append(table);
        // updateById
        StringBuilder updateByIdMethod = new StringBuilder("<update id=\"updateById\" parameterType=\"")
                .append(className)
                .append("\">")
                .append(LF)
                .append("UPDATE ")
                .append(table)
                .append(LF)
                .append("<set>")
                .append(LF);
        // updateByIdSelective
        StringBuilder updateByIdSelectiveMethod = new StringBuilder("<update id=\"updateByIdSelective\" parameterType=\"")
                .append(className)
                .append("\">")
                .append(LF)
                .append("UPDATE ")
                .append(table)
                .append(LF)
                .append("<set>")
                .append(LF);

        // 数据转换
        List<ColumnProperty> cpList = ColumnProperty.columnPropertyList(columnList, dbHelper::getJavaType);
        for (ColumnProperty cp : cpList) {
            // entity
            entity.append("/**\n* ")
                    .append(cp.getColumnComment())
                    .append("\n*/\n")
                    .append("private ")
                    .append(cp.getPropertySimpleType())
                    .append(" ")
                    .append(cp.getPropertyName())
                    .append(";\n\n");
            // resultMap
            baseMap.append("<")
                    .append("id".equals(cp.getColumnName()) ? "id" : "result")
                    .append(" property=\"")
                    .append(cp.getPropertyName())
                    .append("\" column=\"")
                    .append(cp.getColumnName())
                    .append("\" javaType=\"")
                    .append(cp.getPropertyType())
                    .append("\" />")
                    .append(LF);
            // columnSql
            baseColumn.append(cp.getColumnName())
                    .append(',');
            // 不需要对id做处理的
            if (!"id".equals(cp.getColumnName())) {
                // insertColumn
                insertColumn.append(cp.getColumnName())
                        .append(',')
                        .append(LF);
                // insertProperty
                insertProperty.append("#{")
                        .append(cp.getPropertyName())
                        .append("},")
                        .append(LF);
                // replaceColumn
                replaceColumn.append(String.format("%s=VALUES(%s),%n", cp.getColumnName(), cp.getColumnName()));
                // updateById
                updateByIdMethod.append(cp.getColumnName())
                        .append(" = #{")
                        .append(cp.getPropertyName())
                        .append("},")
                        .append(LF);
                // updateByIdSelective
                updateByIdSelectiveMethod.append("<if test=\"")
                        .append(cp.getPropertyName())
                        .append(" != null")
                        .append("String".equals(cp.getPropertySimpleType()) ? (" and " + cp.getPropertyName() + " != ''") : "")
                        .append("\">")
                        .append(LF)
                        .append(cp.getColumnName())
                        .append(" = ")
                        .append("#{")
                        .append(cp.getPropertyName())
                        .append("},")
                        .append(LF)
                        .append("</if>")
                        .append(LF);
            }
        }
        // entity
        entity.append('}');
        // baseMap
        baseMap.append("</resultMap>")
                .append(LF);
        // baseColumn
        baseColumn.deleteCharAt(baseColumn.length() - 1)
                .append(LF)
                .append("</sql>")
                .append(LF);
        // selectById
        selectByIdMethod.append(cpList.get(0).getPropertyType())
                .append("\" resultMap=\"BaseMap\">")
                .append(LF)
                .append("SELECT <include refid=\"BaseColumn\" />")
                .append(LF)
                .append("FROM ")
                .append(table)
                .append(LF)
                .append("WHERE id = #{id}")
                .append(LF)
                .append("</select>")
                .append(LF);
        // insert
        insertColumn.delete(insertColumn.length() - 2, insertColumn.length());
        insertProperty.delete(insertProperty.length() - 2, insertProperty.length());
        insertMethod.append('(')
                .append(insertColumn)
                .append(')')
                .append(LF)
                .append("VALUES")
                .append('(')
                .append(insertProperty)
                .append(')')
                .append(LF)
                .append("</insert>")
                .append(LF);
        // insertBatch
        insertBatchMethod.append('(')
                .append(insertColumn)
                .append(')')
                .append(LF)
                .append("VALUES")
                .append(LF)
                .append("<foreach collection=\"list\" item=\"item\" separator=\"),(\" open=\"(\" close=\")\">")
                .append(LF)
                .append(insertProperty.toString().replaceAll("#\\{", "#{item."))
                .append(LF)
                .append("</foreach>")
                .append(LF)
                .append("</insert>")
                .append(LF);
        // insertOrUpdateBatch
        insertOrUpdateBatchMethod.append('(')
                .append("id,")
                .append(LF)
                .append(insertColumn)
                .append(')')
                .append(LF)
                .append("VALUES")
                .append(LF)
                .append("<foreach collection=\"list\" item=\"item\" separator=\"),(\" open=\"(\" close=\")\">")
                .append(LF)
                .append("#{item.id},")
                .append(LF)
                .append(insertProperty.toString().replaceAll("#\\{", "#{item."))
                .append(LF)
                .append("</foreach>")
                .append(LF)
                .append("ON DUPLICATE KEY UPDATE")
                .append(LF)
                .append(replaceColumn.delete(replaceColumn.length() - 3, replaceColumn.length()))
                .append(LF)
                .append("</insert>")
                .append(LF);
        // updateById
        updateByIdMethod.append("</set>")
                .append(LF)
                .append("WHERE id = #{id}")
                .append(LF)
                .append("</update>")
                .append(LF);
        // updateByIdSelective
        updateByIdSelectiveMethod.append("</set>")
                .append(LF)
                .append("WHERE id = #{id}")
                .append(LF)
                .append("</update>")
                .append(LF);
        // deleteById
        deleteByIdMethod.append(LF)
                .append("DELETE FROM ")
                .append(table)
                .append(LF)
                .append("WHERE id IN ")
                .append(LF)
                .append("<foreach collection=\"idList\" item=\"item\" separator=\",\" open=\"(\" close=\")\">#{item}</foreach>")
                .append("</delete>")
                .append(LF);
        // mapperXml
        mapperXml.append(LF)
                .append(baseMap)
                .append(LF)
                .append(baseColumn)
                .append(LF)
                .append(selectByIdMethod)
                .append(LF)
                .append(insertMethod)
                .append(LF)
                .append(insertBatchMethod)
                .append(LF)
                .append(insertOrUpdateBatchMethod)
                .append(LF)
                .append(updateByIdMethod)
                .append(LF)
                .append(updateByIdSelectiveMethod)
                .append(LF)
                .append(deleteByIdMethod)
                .append(LF)
                .append("</mapper>");

        // mapper
        mapper.append("/**")
                .append(LF)
                .append("* selectById")
                .append(LF)
                .append("* @param id id")
                .append(LF)
                .append("* @return ")
                .append(simpleClassName)
                .append(LF)
                .append("*/")
                .append(LF)
                .append(simpleClassName)
                .append(" selectById(@Param(\"id\") ")
                .append(cpList.get(0).getPropertySimpleType())
                .append(" id);")
                .append(LF)

                .append("/**")
                .append(LF)
                .append("* insert")
                .append(LF)
                .append("* @param ")
                .append(objName)
                .append(' ')
                .append(simpleClassName)
                .append(LF)
                .append("* @return ")
                .append("effect")
                .append(LF)
                .append("*/")
                .append(LF)
                .append("int insert(")
                .append(simpleClassName)
                .append(' ')
                .append(objName)
                .append(");")
                .append(LF)

                .append("/**")
                .append(LF)
                .append("* insertBatch")
                .append(LF)
                .append("* @param list ")
                .append(simpleClassName)
                .append(" List ")
                .append(LF)
                .append("* @return ")
                .append("effect")
                .append(LF)
                .append("*/")
                .append(LF)
                .append("int insertBatch(@Param(\"list\") List<")
                .append(simpleClassName)
                .append("> list);")
                .append(LF)

                .append("/**")
                .append(LF)
                .append("* insertOrUpdateBatch")
                .append(LF)
                .append("* @param list ")
                .append(simpleClassName)
                .append(" List ")
                .append(LF)
                .append("* @return ")
                .append("effect")
                .append(LF)
                .append("*/")
                .append(LF)
                .append("int insertOrUpdateBatch(@Param(\"list\") List<")
                .append(simpleClassName)
                .append("> list);")
                .append(LF)

                .append("/**")
                .append(LF)
                .append("* updateById")
                .append(LF)
                .append("* @param ")
                .append(objName)
                .append(' ')
                .append(simpleClassName)
                .append(LF)
                .append("* @return ")
                .append("effect")
                .append(LF)
                .append("*/")
                .append(LF)
                .append("int updateById(")
                .append(simpleClassName)
                .append(' ')
                .append(objName)
                .append(");")
                .append(LF)

                .append("/**")
                .append(LF)
                .append("* updateByIdSelective")
                .append(LF)
                .append("* @param ")
                .append(objName)
                .append(' ')
                .append(simpleClassName)
                .append(LF)
                .append("* @return ")
                .append("effect")
                .append(LF)
                .append("*/")
                .append(LF)
                .append("int updateByIdSelective(")
                .append(simpleClassName)
                .append(' ')
                .append(objName)
                .append(");")
                .append(LF)

                .append("/**")
                .append(LF)
                .append("* deleteById")
                .append(LF)
                .append("* @param idList idList")
                .append(LF)
                .append("* @return ")
                .append("effect")
                .append(LF)
                .append("*/")
                .append(LF)
                .append("int deleteById(@Param(\"idList\") List<")
                .append(cpList.get(0).getPropertySimpleType())
                .append("> idList);")
                .append(LF)

                .append('}');

        PrintUtil.separatorLine();
        String mapperFileName = OUTPUT_PATH + simpleMapperName + ".java";
        FileUtil.writeTxt(mapperFileName, mapper.toString());
        String xmlFileName = OUTPUT_PATH + simpleClassName + "Mapper.xml";
        FileUtil.writeTxt(xmlFileName, mapperXml.toString());
        String javaFileName = OUTPUT_PATH + simpleClassName + ".java";
        FileUtil.writeTxt(javaFileName, entity.toString());
        PrintUtil.separatorLine();
    }

    /**
     * 开始多次操作
     *
     * @param dbServer DatabaseServerEnum
     * @param batch    是否是批处理
     */
    private static void startMultipleOperation(DatabaseServerEnum dbServer, Boolean batch) {
        if (null != dbServer) {
            setDb(dbServer);
        }
        session = null == batch ? getSession() : getSession(batch);
    }

    /**
     * 开始多次操作
     *
     * @param batch 是否是批处理
     */
    private static void stopMultipleOperation(Boolean batch) {
        if (null == batch) {
            closeSession();
        } else {
            closeSession(batch);
        }
    }

    /**
     * 获取sql session
     *
     * @return SqlSession
     */
    private static SqlSession getSession() {
        return getSession(false);
    }

    /**
     * 获取sql session
     *
     * @param batch 是否是批处理
     * @return SqlSession
     */
    private static SqlSession getSession(boolean batch) {
        SqlSession session = null;
        try (Reader reader = Resources.getResourceAsReader("mybatisConf/mybatisConf.xml")) {
            SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader, env);
            session = sessionFactory.openSession(batch ? ExecutorType.BATCH : ExecutorType.SIMPLE, !batch);
        } catch (Exception e) {
            PrintUtil.log("getSession batch : {} error ", batch, e);
        }
        return session;
    }

    /**
     * 关闭session
     */
    private static void closeSession() {
        session.commit();
    }

    /**
     * 关闭session
     *
     * @param batch 是否是批处理
     */
    private static void closeSession(boolean batch) {
        if (batch) {
            session.commit();
        }
        session.close();
    }
}